import sys
import os
import json
import subprocess
from urllib.parse import urlparse
from multiprocessing import freeze_support
import logging
import threading

from PySide6.QtCore import (QTimer, QPoint, QSettings, Slot, QRect, QThread, Signal, QSize,
                            Qt, QRectF, QPointF, QPropertyAnimation, QEasingCurve, QUrl, QMimeData)
from PySide6.QtGui import (QContextMenuEvent, QFont, QPixmap, QMovie, QAction, QIcon, QPalette,
                           QColor, QPainter, QBrush, QActionGroup, QPolygonF, QDrag, QPen, QDropEvent, QDragEnterEvent)
from PySide6.QtWidgets import (
    QApplication, QWidget, QLabel, QVBoxLayout, QPushButton,
    QMenu, QFileDialog, QDialog, QHBoxLayout, QLineEdit,
    QListWidget, QListWidgetItem, QInputDialog, QMessageBox,
    QSlider, QStyle, QCheckBox, QTabWidget, QTextEdit, QProgressDialog, QFrame,
    QGraphicsOpacityEffect
)

from flask import Flask, request, jsonify
from werkzeug.serving import make_server
from openai import OpenAI

# 常量和样式表
APP_DATA_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(APP_DATA_DIR, "./.json/system.json")
API_CONFIG_FILE = os.path.join(APP_DATA_DIR, "./.json/api_config.json")
KNOWLEDGE_BASE_DIR = os.path.join(APP_DATA_DIR, "./.knowledge_bases")
PLUGIN_STORAGE_DIR = os.path.join(APP_DATA_DIR, "./plug-in")
DEFAULT_VENV_DIR = os.path.join(APP_DATA_DIR, "./.venv")
CUSTOM_PLAY_ICON_PATH = os.path.join(APP_DATA_DIR, "./images/play.png")
CUSTOM_STOP_ICON_PATH = os.path.join(APP_DATA_DIR, "./images/stop.png")

if not os.path.exists(PLUGIN_STORAGE_DIR):
    os.makedirs(PLUGIN_STORAGE_DIR)
if not os.path.exists(KNOWLEDGE_BASE_DIR):
    os.makedirs(KNOWLEDGE_BASE_DIR)
if not os.path.exists("./.json"):
    os.makedirs("./.json")
DEFAULT_PET_BASE_WIDTH, DEFAULT_PET_BASE_HEIGHT = 100, 100
DEFAULT_PET_IMAGE_FALLBACK = "default_pet_fallback.png"
STYLESHEET = """
QWidget { font-family: "Microsoft YaHei", "Segoe UI", Arial, sans-serif; color: #E0E0E0; } QDialog, QTabWidget, DesktopPet { background-color: #2E2E2E; border-radius: 8px; } DesktopPet { background-color: transparent; } QLabel { background-color: transparent; color: #D0D0D0; padding: 2px; } QLineEdit, QTextEdit { background-color: #3C3C3C; border: 1px solid #555555; border-radius: 4px; padding: 5px; color: #E0E0E0; } QLineEdit:focus, QTextEdit:focus { border: 1px solid #0078D7; } QPushButton { background-color: #4A4A4A; color: #EFEFEF; border: 1px solid #606060; border-radius: 4px; padding: 6px 12px; min-height: 20px; } QPushButton:hover { background-color: #5A5A5A; border-color: #707070; } QPushButton:pressed { background-color: #3A3A3A; } QPushButton:disabled { background-color: #383838; color: #777777; } QPushButton#ToggleButton { font-weight: bold; } QPushButton#ToggleButton[checked="true"] { background-color: #4CAF50; border-color: #388E3C; } QPushButton#ToggleButton[checked="false"] { background-color: #F44336; border-color: #D32F2F; } QPushButton#ToggleButton:hover { opacity: 0.9; } QMenu::item { color: #D0D0D0; } QMenu::item.inactive { color: #777777; } QListWidget { background-color: #3C3C3C; border: 1px solid #555555; border-radius: 4px; alternate-background-color: #424242; } QListWidget::item { padding: 5px; color: #D0D0D0; border-bottom: 1px solid #484848; } QListWidget::item:hover { background-color: #4F4F4F; } QListWidget::item:selected { background-color: transparent; color: #D0D0D0; } QMenu { background-color: #3C3C3C; border: 1px solid #555555; padding: 4px; border-radius: 4px; font-size: 13px; } QMenu::item { padding: 6px 20px 6px 20px; min-width: 130px; font-size: 13px; } QMenu::item:selected { background-color: #0078D7; color: #FFFFFF; } QMenu::item:disabled { color: #777777; } QMenu::separator { height: 1px; background-color: #555555; margin-top: 4px; margin-bottom: 4px; } QMenu::icon { padding-left: 5px; } QSlider::groove:horizontal { border: 1px solid #555555; height: 8px; background: #303030; margin: 2px 0; border-radius: 4px; } QSlider::handle:horizontal { background: #0078D7; border: 1px solid #005A9E; width: 16px; height: 16px; margin: -4px 0; border-radius: 8px; } QSlider::handle:horizontal:hover { background: #008CFF; } QMessageBox, QInputDialog { background-color: #2E2E2E; color: #E0E0E0; border-radius: 8px; } QMessageBox QLabel, QInputDialog QLabel { color: #D0D0D0; } QMessageBox QPushButton, QInputDialog QPushButton { min-width: 70px; background-color: #4A4A4A; } QMessageBox QPushButton:hover, QInputDialog QPushButton:hover { background-color: #5A5A5A; } QMessageBox QPushButton:pressed, QInputDialog QPushButton:pressed { background-color: #3A3A3A; } QInputDialog QLineEdit { background-color: #3C3C3C; border: 1px solid #555555; color: #E0E0E0; } QTabWidget::pane { border: 1px solid #444; border-top: 0px; } QTabBar::tab { background-color: #3C3C3C; padding: 8px 20px; border: 1px solid #555; border-bottom: none; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:selected { background-color: #2E2E2E; border-color: #444; } QTabBar::tab:!selected { background-color: #4A4A4A; color: #B0B0B0; } QTabBar::tab:!selected:hover { background-color: #5A5A5A; }
"""
DEFAULT_HELPER_AI_PROMPT = """你是一个高效的知识库检索助理。你的任务是：从[原始知识库]中，提取出所有与[用户问题]相关的信息(只要有一点有关就算，宁可杀错不可放过)原封不动的输出来(初次之外不要其他语句)；如果找不到任何相关信息，请直接回复"未找到相关知识。"。"""


def print_dict_as_json(data, title=""):
    header = f"---------- {title} ----------"
    footer = "-" * len(header)
    try:
        formatted_json = json.dumps(data, indent=2, ensure_ascii=False)
        print(header)
        print(formatted_json)
        print(footer)
    except Exception as e:
        print(f"{header}\n无法格式化为JSON: {e}\n{data}\n{footer}")


class BubbleLabel(QLabel):
    """消息气泡"""
    def __init__(self, pet_widget):
        super().__init__()
        self.pet_widget = pet_widget
        self.setWindowFlags(
            Qt.WindowType.FramelessWindowHint |
            Qt.WindowType.Tool |
            Qt.WindowType.WindowStaysOnTopHint
        )
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)

        self.horizontal_padding = 12
        self.top_padding = 8
        self.bottom_padding = 12
        self.border_radius = 10
        self.pointer_height = 3
        self.pointer_width = 16

        self.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
        self.setStyleSheet(f"""
            color: #EFEFEF;
            font-size: 13px;
            font-family: "Microsoft YaHei";
            padding-top: {self.top_padding}px;
        """)

        #  动画设置
        self.opacity_effect = QGraphicsOpacityEffect(self)
        self.opacity_effect.setOpacity(0.0)
        self.setGraphicsEffect(self.opacity_effect)

        self.fade_out_animation = QPropertyAnimation(self.opacity_effect, b"opacity", self)
        self.fade_out_animation.setDuration(500)
        self.fade_out_animation.setStartValue(1.0)
        self.fade_out_animation.setEndValue(0.0)
        self.fade_out_animation.setEasingCurve(QEasingCurve.Type.InQuad)
        self.fade_out_animation.finished.connect(self.hide)

        self.popup_animation = QPropertyAnimation(self, b"geometry")
        self.popup_animation.setDuration(400)
        self.popup_animation.setEasingCurve(QEasingCurve.Type.OutBack)

        self.hide_timer = QTimer(self)
        self.hide_timer.setSingleShot(True)
        self.hide_timer.timeout.connect(self.fade_out_animation.start)

    def paintEvent(self, event):
        """绘制气泡"""
        painter = QPainter(self)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        bg_color = QColor(46, 46, 46, 235)
        painter.setBrush(QBrush(bg_color))
        painter.setPen(Qt.PenStyle.NoPen)
        rect = self.rect()
        bubble_rect = QRectF(
            rect.left(), rect.top(),
            rect.width(), rect.height() - self.pointer_height
        )
        painter.drawRoundedRect(bubble_rect, self.border_radius, self.border_radius)
        pointer_poly = QPolygonF()
        pointer_start_x = (rect.width() - self.pointer_width) / 2
        pointer_poly.append(QPointF(pointer_start_x, rect.height() - self.pointer_height))
        pointer_poly.append(QPointF(pointer_start_x + self.pointer_width / 2, rect.height()))
        pointer_poly.append(QPointF(pointer_start_x + self.pointer_width, rect.height() - self.pointer_height))
        painter.drawPolygon(pointer_poly)
        super().paintEvent(event)

    def show_message(self, text, duration_ms=2500):
        """显示消息"""
        self.hide_timer.stop()
        self.popup_animation.stop()
        self.fade_out_animation.stop()
        self.setText(text)
        self.opacity_effect.setOpacity(1.0)
        self.show()

        fm = self.fontMetrics()
        text_width = fm.boundingRect(text).width()
        text_height = fm.height()

        final_width = text_width + self.horizontal_padding * 2
        final_height = self.top_padding + text_height + self.bottom_padding + self.pointer_height

        pet_pos = self.pet_widget.pos()
        pet_width = self.pet_widget.width()
        final_x = pet_pos.x() + (pet_width - final_width) / 2
        final_y = pet_pos.y() - final_height - 3
        final_geom = QRect(int(final_x), int(final_y), int(final_width), int(final_height))

        start_geom = QRect(
            int(final_x + final_width / 2),
            int(final_y + final_height - self.pointer_height),
            0,
            0
        )
        self.popup_animation.setStartValue(start_geom)
        self.popup_animation.setEndValue(final_geom)
        self.popup_animation.start()
        self.hide_timer.start(duration_ms)

    def reposition(self):
        """移动时，重新定位气泡位置。"""
        if self.popup_animation.state() == QPropertyAnimation.State.Running or \
                self.fade_out_animation.state() == QPropertyAnimation.State.Running:
            return
        if not self.pet_widget or not self.isVisible():
            return
        my_width = self.width()
        my_height = self.height()
        pet_pos = self.pet_widget.pos()
        pet_width = self.pet_widget.width()
        x = pet_pos.x() + (pet_width - my_width) / 2
        y = pet_pos.y() - my_height - 10
        self.move(int(x), int(y))


class KnowledgeApiService(QThread):
    """运行Flask Web服务，为插件提供知识库访问API"""
    message_from_plugin_received = Signal(str)

    def __init__(self, api_manager, kb_manager, host='127.0.0.1', port=5678, parent=None):
        super().__init__(parent)
        self.api_manager = api_manager
        self.kb_manager = kb_manager
        self.host = host
        self.port = port
        self.flask_app = Flask(__name__)
        log = logging.getLogger('werkzeug')
        log.setLevel(logging.ERROR)
        self.server = make_server(self.host, self.port, self.flask_app, threaded=True)
        self.ctx = self.flask_app.app_context()
        self.ctx.push()
        self.flask_app.route('/api/knowledge', methods=['POST'])(self.get_knowledge)

        # 【新增代码】为显示消息功能注册一个新的API路由
        self.flask_app.route('/api/show_message', methods=['POST'])(self.show_message_from_plugin)

    def run(self):
        print(f"✅ [主程序] API服务已在 http://{self.host}:{self.port} 上启动。")
        self.server.serve_forever()

    def stop(self):
        shutdown_thread = threading.Thread(target=self.server.shutdown)
        shutdown_thread.start()
        print("🛑 [主程序] API服务已停止。")

    def show_message_from_plugin(self):
        """接收插件发来的消息并使用信号将其发送到主线程。"""
        if not request.is_json:
            return jsonify({"error": "请求必须是JSON格式"}), 400

        data = request.get_json()
        message = data.get("message")

        if not message:
            return jsonify({"error": "请求中缺少 'message'"}), 400

        self.message_from_plugin_received.emit(message)

        print(f"💬 [主程序] 收到插件消息: '{message}'")
        return jsonify({"status": "message_sent"}), 200

    def get_knowledge(self):
        """处理来自插件的知识库检索请求的API端点。"""
        print("\n" + "=" * 50 + "\n[主程序] 知识库服务收到新请求..." + "\n" + "=" * 50)

        if not request.is_json:
            print("  [步骤1 - 失败] 请求格式不是JSON，已拒绝。")
            return jsonify({"error": "请求必须是JSON格式"}), 400

        data = request.get_json()
        print_dict_as_json(data, "步骤1 - 成功: 收到的请求内容")
        user_query = data.get("user_query")
        plugin_name = data.get("plugin_name")

        if not user_query or not plugin_name:
            print("  [步骤1 - 失败] 请求中缺少'user_query'或'plugin_name'，已拒绝。")
            return jsonify({"error": "请求中缺少 'user_query' 或 'plugin_name'"}), 400

        is_kb_enabled = self.is_plugin_kb_enabled(plugin_name)
        print(f"  [步骤2 - 检查] 插件 '{plugin_name}' 的知识库开关状态为: {is_kb_enabled}")
        if not is_kb_enabled:
            print("  [步骤2 - 终止] 因开关关闭，直接返回空知识。")
            return jsonify({"relevant_knowledge": ""})

        full_kb_content = self.kb_manager.get_active_base_content()
        print(f"  [步骤3 - 检查] 当前激活的知识库是: '{self.kb_manager.get_active_base_name() or '无'}'")
        if not full_kb_content:
            print("  [步骤3 - 终止] 激活的知识库内容为空，返回空知识。")
            return jsonify({"relevant_knowledge": ""})

        print(f"  [步骤3 - 成功] 已读取到激活的知识库内容 (长度: {len(full_kb_content)} 字符)。")

        try:
            relevant_text = self.retrieve_with_helper_ai(user_query, full_kb_content)
            print("  [步骤5 - 成功] 已将检索结果发送回插件。")
            return jsonify({"relevant_knowledge": relevant_text})
        except Exception as e:
            print(f"  [步骤4 - 失败] 知识检索时发生严重错误: {e}")
            return jsonify({"error": str(e)}), 500

    def is_plugin_kb_enabled(self, plugin_name):
        """从主配置文件中检查对应插件是否启用了知识库。"""
        try:
            if not os.path.exists(CONFIG_FILE): return False
            with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                config = json.load(f)
            for p_config in config.get("plugins", []):
                config_name = p_config.get("name")
                if config_name == plugin_name:
                    is_enabled = p_config.get("use_kb", False)
                    return is_enabled
            print(f"    [检查配置 - 失败] 未在配置文件中找到名为 '{plugin_name}' 的插件。")
            return False
        except (IOError, json.JSONDecodeError) as e:
            print(f"    [检查配置 - 失败] 读取或解析配置文件失败: {e}")
            return False

    def retrieve_with_helper_ai(self, user_query, full_kb_content):
        """调用一个专门的AI模型从知识库中提取与问题相关的内容。"""
        helper_config = self.api_manager.get_kb_helper_config()
        global_config = self.api_manager.get_global_api()

        helper_model = helper_config.get("model", "").strip() or global_config.get("model")
        helper_api_key = helper_config.get("api_key", "").strip() or global_config.get("api_key")
        helper_base_url = helper_config.get("base_url", "").strip() or global_config.get("base_url")
        helper_prompt = helper_config.get("prompt")

        print(f"  [步骤4 - 检索] 使用辅助AI模型 '{helper_model}' 进行知识检索。")
        if not helper_model or not helper_api_key:
            print("    [检索 - 警告] 辅助AI未配置，将返回全部知识库内容作为备用方案。")
            return full_kb_content

        helper_client = OpenAI(api_key=helper_api_key, base_url=helper_base_url if helper_base_url else None)
        structured_prompt = f"[原始知识库]\n{full_kb_content}\n\n[用户问题]\n{user_query}"

        messages_to_helper = [{"role": "system", "content": helper_prompt},
                              {"role": "user", "content": structured_prompt}]
        print_dict_as_json(messages_to_helper, "步骤4.1 - 发送给辅助AI的Messages")

        response = helper_client.chat.completions.create(model=helper_model, messages=messages_to_helper,
                                                         temperature=0.0)
        relevant_text = response.choices[0].message.content

        print_dict_as_json({"content": relevant_text}, "步骤4.2 - 从辅助AI收到的回复")

        if "未找到相关知识" in relevant_text:
            print("    [检索 - 完成] 辅助AI未能从知识库中检索到相关信息。")
            return ""
        print("    [检索 - 完成] 辅助AI已成功检索到相关知识片段。")
        return relevant_text


class KnowledgeBaseEditorDialog(QDialog):
    """编辑知识库文本的窗口"""
    def __init__(self, base_name, initial_content, parent=None):
        super().__init__(parent)
        self.setWindowTitle(f"编辑知识库: {base_name}")
        self.setMinimumSize(600, 500)
        main_layout = QVBoxLayout(self)
        self.text_edit = QTextEdit(self)
        self.text_edit.setPlainText(initial_content)
        main_layout.addWidget(self.text_edit)
        button_layout = QHBoxLayout()
        save_button = QPushButton("保存并关闭")
        save_button.clicked.connect(self.accept)
        cancel_button = QPushButton("取消")
        cancel_button.clicked.connect(self.reject)
        button_layout.addStretch()
        button_layout.addWidget(save_button)
        button_layout.addWidget(cancel_button)
        main_layout.addLayout(button_layout)

    def get_content(self):
        """获取文本框中的内容。"""
        return self.text_edit.toPlainText()


class ApiManager:
    """管理所有API配置"""
    def __init__(self, config_path):
        self.config_path = config_path
        self.config = self._load_config()

    def _load_config(self):
        """从JSON文件加载配置"""
        try:
            if os.path.exists(self.config_path):
                with open(self.config_path, 'r', encoding='utf-8') as f: return json.load(f)
        except:
            pass
        return {"global": {"api_key": "", "base_url": "", "model": ""},
                "knowledge_base_helper": {"api_key": "", "base_url": "", "model": "",
                                          "prompt": DEFAULT_HELPER_AI_PROMPT},
                "plugins": {}}

    def _save_config(self):
        """将当前配置保存到JSON文件。"""
        try:
            with open(self.config_path, 'w', encoding='utf-8') as f:
                json.dump(self.config, f, indent=4, ensure_ascii=False)
        except Exception as e:
            print(f"保存API配置失败: {e}")

    def get_global_api(self):
        """获取全局API设置"""
        default = {"api_key": "", "base_url": "", "model": ""}
        config = self.config.get("global", default)
        for k, v in default.items():
            config.setdefault(k, v)
        return config

    def set_global_api(self, api_key, base_url, model):
        """设置并保存全局API配置。"""
        self.config["global"] = {"api_key": api_key, "base_url": base_url, "model": model}
        self._save_config()

    def get_kb_helper_config(self):
        """获取知识库辅助AI的配置。"""
        default = {"api_key": "", "base_url": "", "model": "", "prompt": DEFAULT_HELPER_AI_PROMPT}
        config = self.config.get("knowledge_base_helper", default)
        for k, v in default.items(): config.setdefault(k, v)
        return config

    def set_kb_helper_config(self, api_key, base_url, model, prompt):
        """设置并保存知识库辅助AI的配置。"""
        self.config["knowledge_base_helper"] = {"api_key": api_key, "base_url": base_url, "model": model,
                                                "prompt": prompt}
        self._save_config()

    def get_plugin_api(self, plugin_name):
        """获取指定插件的专属API配置"""
        return self.config.get("plugins", {}).get(plugin_name)

    def set_plugin_api(self, plugin_name, api_key, base_url, model):
        """设置并保存指定插件的专属API配置"""
        if "plugins" not in self.config:
            self.config["plugins"] = {}
        self.config["plugins"][plugin_name] = {"api_key": api_key, "base_url": base_url, "model": model}
        self._save_config()

    def remove_plugin_api(self, plugin_name):
        """移除指定插件的专属API配置。"""
        if self.config.get("plugins", {}).pop(plugin_name, None):
            self._save_config()

    def get_resolved_api_config(self, plugin_name):
        """获取最终生效的API配置"""
        plugin_api = self.get_plugin_api(plugin_name) or {}
        global_api = self.get_global_api()
        api_key = plugin_api.get("api_key", "").strip() or global_api.get("api_key", "")
        base_url = plugin_api.get("base_url", "").strip() or global_api.get("base_url", "")
        model = plugin_api.get("model", "").strip() or global_api.get("model", "")
        return api_key, base_url, model


class KnowledgeBaseManager:
    """管理知识库文件的类"""
    def __init__(self, base_dir, settings):
        self.base_dir = base_dir
        self.settings = settings
        if not os.path.exists(self.base_dir):
            os.makedirs(self.base_dir)

    def append_to_base(self, name, new_content):
        """向指定的知识库文件追加内容"""
        if not name: return False, "没有激活的知识库"
        file_path = os.path.join(self.base_dir, f"{name}.txt")
        if not os.path.exists(file_path):
            return False, f"知识库 '{name}' 文件不存在。"
        try:
            with open(file_path, 'a', encoding='utf-8') as f:
                f.write(f"\n\n{new_content.strip()}")
            return True, f"内容已成功添加到知识库 '{name}'！"
        except IOError as e:
            return False, f"无法写入知识库文件: {e}"

    def update_base_content(self, name, content):
        """用新内容完全覆盖指定的知识库文件"""
        file_path = os.path.join(self.base_dir, f"{name}.txt")
        if not os.path.exists(file_path): return False, "知识库文件不存在"
        try:
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(content)
            return True, "保存成功"
        except IOError as e:
            return False, f"写入文件失败: {e}"

    def get_all_bases(self):
        """获取所有知识库的名称列表。"""
        try:
            return [f.replace('.txt', '') for f in os.listdir(self.base_dir) if f.endswith('.txt')]
        except FileNotFoundError:
            return []

    def create_base(self, name):
        """创建一个空知识库文件。"""
        if not name or not name.strip():
            return False, "名称不能为空"
        file_path = os.path.join(self.base_dir, f"{name}.txt")
        if os.path.exists(file_path):
            return False, "同名知识库已存在"
        try:
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write("")
            return True, "创建成功"
        except IOError as e:
            return False, f"创建文件失败: {e}"

    def rename_base(self, old_name, new_name):
        """重命名知识库文件"""
        if not new_name or not new_name.strip(): return False, "新名称不能为空"
        old_path = os.path.join(self.base_dir, f"{old_name}.txt")
        new_path = os.path.join(self.base_dir, f"{new_name}.txt")
        if not os.path.exists(old_path): return False, "原文件不存在"
        if os.path.exists(new_path): return False, "新名称已存在"
        try:
            os.rename(old_path, new_path)
            return True, "重命名成功"
        except OSError as e:
            return False, f"重命名失败: {e}"

    def delete_base(self, name):
        """删除一个知识库文件"""
        file_path = os.path.join(self.base_dir, f"{name}.txt")
        if not os.path.exists(file_path):
            return False, "文件不存在"
        try:
            os.remove(file_path)
            return True, "删除成功"
        except OSError as e:
            return False, f"删除失败: {e}"

    def get_active_base_name(self):
        """获取当前激活的知识库名称"""
        return self.settings.value("knowledgeBase/active", None)

    def set_active_base(self, name):
        """设置当前激活的知识库名称"""
        self.settings.setValue("knowledgeBase/active", name)
        self.settings.sync()

    def get_active_base_content(self):
        """获取当前激活知识库的全部内容"""
        active_base = self.get_active_base_name()
        if not active_base: return None
        file_path = os.path.join(self.base_dir, f"{active_base}.txt")
        if not os.path.exists(file_path):
            return None
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                return f.read()
        except IOError:
            return None

    def get_base_content_by_name(self, name):
        """根据名称获取指定知识库的内容"""
        if not name:
            return None
        file_path = os.path.join(self.base_dir, f"{name}.txt")
        if not os.path.exists(file_path):
            return None
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                return f.read()
        except IOError:
            return None


class PipTaskThread(QThread):
    """pip命令的线程"""
    task_started = Signal(str)
    task_output = Signal(str)
    task_finished = Signal(str)

    def __init__(self, command_list, parent=None):
        super().__init__(parent)
        self.command_list = command_list

    def run(self):
        self.task_started.emit(f"正在执行: {' '.join(self.command_list)}\n")
        try:
            process = subprocess.Popen(self.command_list, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
                                       encoding='utf-8', errors='replace',
                                       creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0)
            for line in iter(process.stdout.readline, ''):
                self.task_output.emit(line.strip())
            process.wait()
            if process.returncode == 0:
                self.task_finished.emit(f"\n命令 {' '.join(self.command_list[:2])}... 执行成功！")
            else:
                self.task_finished.emit(
                    f"\n错误：命令 {' '.join(self.command_list[:2])}... 执行失败，返回码: {process.returncode}")
        except Exception as e:
            self.task_finished.emit(f"\n执行命令时发生未知错误: {e}")


class EnvironmentManager:
    """管理Python环境的类"""
    def __init__(self, settings):
        self.settings = settings
        self.python_executable = self._find_python_executable()

    def _find_python_executable(self):
        """查找有效的Python解释器路径。"""
        path = self.settings.value("pythonExecutablePath", "")
        if self.is_valid_python_env(path):
            return path
        if os.path.exists(DEFAULT_VENV_DIR):
            default_py = self.get_venv_python(DEFAULT_VENV_DIR)
            if self.is_valid_python_env(default_py):
                self.settings.setValue("pythonExecutablePath", default_py)
                return default_py
        return ""

    def get_venv_python(self, venv_path):
        """获取虚拟环境中的python.exe路径"""
        return os.path.join(venv_path, "Scripts", "python.exe") if sys.platform == "win32" else os.path.join(venv_path,
                                                                                                             "bin",
                                                                                                             "python")

    def is_valid_python_env(self, python_path):
        """检查给定的路径是否是一个有效的Python解释器"""
        return python_path and os.path.exists(python_path) and "python" in os.path.basename(python_path).lower()

    def create_default_venv(self, progress_callback, base_python_path):
        """使用指定的系统Python创建默认的虚拟环境"""
        progress_callback("正在移除旧环境...")
        try:
            import shutil
            if os.path.exists(DEFAULT_VENV_DIR): shutil.rmtree(DEFAULT_VENV_DIR)
            progress_callback(f"使用 {os.path.basename(base_python_path)} 创建虚拟环境...")
            cmd = [base_python_path, '-m', 'venv', DEFAULT_VENV_DIR, '--clear']
            subprocess.run(cmd, check=True, capture_output=True, text=True, encoding='utf-8')
            new_python_path = self.get_venv_python(DEFAULT_VENV_DIR)
            if self.is_valid_python_env(new_python_path):
                self.set_python_executable(new_python_path)
                progress_callback(
                    "成功创建虚拟环境！")
                return True, "创建成功！"
            else:
                raise RuntimeError("环境已创建，但找不到python解释器。")
        except Exception as e:
            error_msg = f"创建虚拟环境失败: {e}"
            progress_callback(error_msg)
            return False, error_msg

    def set_python_executable(self, path):
        """设置并保存新的Python解释器路径"""
        if self.is_valid_python_env(path):
            self.python_executable = path
            self.settings.setValue("pythonExecutablePath", path)
            return True
        return False

    def get_pip_executable_path(self):
        """获取当前Python环境对应的pip可执行文件路径"""
        if not self.python_executable: return None
        return os.path.join(os.path.dirname(self.python_executable), 'pip')


class ProcessOutputReader(QThread):
    """实时读取子进程的输出流"""
    line_received = Signal(str)
    process_finished = Signal(int)

    def __init__(self, process, parent=None):
        super().__init__(parent)
        self.process = process

    def run(self):
        for line in iter(self.process.stdout.readline, ''):
            self.line_received.emit(line.strip())
        self.process.wait()
        self.process_finished.emit(self.process.returncode)


class PluginItemWidget(QWidget):
    """插件列表控件"""
    def __init__(self, plugin_name, plugin_path, plugin_dialog, initial_state=False, initial_menu_state=False,
                 use_kb_initial_state=False):
        super().__init__()
        self.plugin_name, self.plugin_path, self.plugin_dialog = plugin_name, plugin_path, plugin_dialog
        self.process, self.output_reader_thread = None, None
        layout = QHBoxLayout(self)
        layout.setContentsMargins(5, 5, 5, 14)
        layout.setSpacing(10)
        self.drag_handle = QLabel("☰")
        self.drag_handle.setFixedWidth(20)
        self.drag_handle.setAlignment(Qt.AlignmentFlag.AlignCenter)
        font = QFont()
        font.setPointSize(14)
        self.drag_handle.setFont(font)
        self.drag_handle.setStyleSheet("color: #AAAAAA;")
        layout.addWidget(self.drag_handle)
        self.name_label = QLabel(self.plugin_name)
        self.name_label.setToolTip(f"路径: {self.plugin_path}")
        layout.addWidget(self.name_label, 1)
        font = self.name_label.font()
        font.setPointSize(12)
        self.name_label.setFont(font)
        self.kb_checkbox = QCheckBox("用知识库")
        self.kb_checkbox.setToolTip("开启后，此插件调用AI时会自动携带当前知识库的内容")
        self.kb_checkbox.setChecked(use_kb_initial_state)
        self.kb_checkbox.stateChanged.connect(self.on_state_changed)
        layout.addWidget(self.kb_checkbox)
        self.menu_checkbox = QCheckBox("加入菜单")
        self.menu_checkbox.setChecked(initial_menu_state)
        self.menu_checkbox.stateChanged.connect(self.on_state_changed)
        layout.addWidget(self.menu_checkbox)
        self.api_settings_button = QPushButton("设置API")
        self.api_settings_button.clicked.connect(self.open_api_settings)
        layout.addWidget(self.api_settings_button)
        self.toggle_button = QPushButton("关")
        self.toggle_button.setObjectName("ToggleButton")
        self.toggle_button.setCheckable(True)
        self.toggle_button.setChecked(initial_state)
        self.toggle_button.setFixedSize(QSize(60, 30))
        font = self.toggle_button.font()
        font.setPointSize(11)
        self.toggle_button.setFont(font)
        self.update_button_text()
        self.toggle_button.toggled.connect(self.on_toggle)
        layout.addWidget(self.toggle_button)
        if initial_state: QTimer.singleShot(100, self.start_plugin)

    def on_state_changed(self):
        """复选框状态改变时，立刻保存所有插件的配置"""
        self.plugin_dialog.save_plugins_config()

    def open_api_settings(self):
        """插件专属的API设置框"""
        dialog = QDialog(self)
        dialog.setWindowTitle(f"'{self.plugin_name}' 的主AI设置")
        layout = QVBoxLayout(dialog)
        api_manager = self.plugin_dialog.pet_window.api_manager
        plugin_api = api_manager.get_plugin_api(self.plugin_name) or {}
        layout.addWidget(QLabel("留空则使用全局API设置"))
        key_layout = QHBoxLayout()
        key_layout.addWidget(QLabel("API Key:"))
        key_edit = QLineEdit(plugin_api.get("api_key", ""))
        key_layout.addWidget(key_edit)
        layout.addLayout(key_layout)
        url_layout = QHBoxLayout()
        url_layout.addWidget(QLabel("Base URL:"))
        url_edit = QLineEdit(plugin_api.get("base_url", ""))
        url_layout.addWidget(url_edit)
        layout.addLayout(url_layout)
        model_layout = QHBoxLayout()
        model_layout.addWidget(QLabel("模型名称:"))
        model_edit = QLineEdit(plugin_api.get("model", ""))
        model_layout.addWidget(model_edit)
        layout.addLayout(model_layout)
        button_box = QHBoxLayout()
        save_button = QPushButton("保存")
        save_button.clicked.connect(lambda: (
            api_manager.set_plugin_api(self.plugin_name, key_edit.text().strip(), url_edit.text().strip(),
                                       model_edit.text().strip()), dialog.accept()))
        cancel_button = QPushButton("取消")
        cancel_button.clicked.connect(dialog.reject)
        button_box.addStretch()
        button_box.addWidget(save_button)
        button_box.addWidget(cancel_button)
        layout.addLayout(button_box)
        dialog.exec()

    def start_plugin(self):
        """启动插件子进程"""
        if self.process and self.process.poll() is None:
            return
        venv_python = self.plugin_dialog.pet_window.env_manager.python_executable
        if not venv_python:
            QMessageBox.critical(self, "环境错误", "未配置有效的Python虚拟环境。")
            self.toggle_button.setChecked(False)
            return
        plugin_runner_path = os.path.join(APP_DATA_DIR, "plugin_runner.py")
        if not os.path.exists(plugin_runner_path):
            QMessageBox.critical(self, "启动器丢失", "找不到插件启动器 'plugin_runner.py'。")
            self.toggle_button.setChecked(False)
            return
        plugin_env = os.environ.copy()
        api_manager = self.plugin_dialog.pet_window.api_manager
        api_key, base_url, model_name = api_manager.get_resolved_api_config(self.plugin_name)
        plugin_env["AIA_API_KEY"] = api_key or ""
        plugin_env["AIA_API_BASE_URL"] = base_url or ""
        plugin_env["AIA_MODEL_NAME"] = model_name or ""
        service = self.plugin_dialog.pet_window.kb_api_service
        if service and service.isRunning():
            plugin_env["AIA_KNOWLEDGE_SERVICE_URL"] = f"http://{service.host}:{service.port}"
        try:
            command = [venv_python, plugin_runner_path, self.plugin_path, self.plugin_name]
            self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
                                            encoding='utf-8', errors='replace', env=plugin_env,
                                            creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0)
            self.output_reader_thread = ProcessOutputReader(self.process)
            self.output_reader_thread.line_received.connect(self.handle_process_output)
            self.output_reader_thread.process_finished.connect(self.handle_process_finished)
            self.output_reader_thread.start()
            self.plugin_dialog.pet_window.show_bubble_message(f"插件 [{self.plugin_name}] 已启动")
        except Exception as e:
            QMessageBox.critical(self, "插件启动失败", f"无法启动插件 '{self.plugin_name}':\n{e}")
            self.toggle_button.setChecked(False)
            self.update_button_text()

    def get_state(self):
        """获取当前插件的所有状态，用于保存到配置文件"""
        return {"name": self.plugin_name, "path": self.plugin_path, "active": self.toggle_button.isChecked(),
                "in_menu": self.menu_checkbox.isChecked(), "use_kb": self.kb_checkbox.isChecked()}

    def update_button_text(self):
        """根据开关状态更新按钮的文本（开/关）和样式。"""
        is_checked = self.toggle_button.isChecked()
        self.toggle_button.setText("开" if is_checked else "关")
        self.toggle_button.style().unpolish(self.toggle_button)
        self.toggle_button.style().polish(self.toggle_button)

    @Slot(bool)
    def on_toggle(self, checked):
        """启停开关被点击时的槽函数。"""
        self.update_button_text()
        pet_window = self.plugin_dialog.pet_window
        if checked:
            self.start_plugin()
        else:
            self.stop_plugin()
            pet_window.show_bubble_message(f"插件 [{self.plugin_name}] 已关闭")
        self.on_state_changed()

    @Slot(str)
    def handle_process_output(self, line):
        """处理来自插件子进程的输出"""
        print(f"[{self.plugin_name}]: {line}")
        if line.startswith("ERROR::"):
            error_message = line[7:]
            QMessageBox.warning(self.plugin_dialog, f"插件 '{self.plugin_name}' 运行时错误", error_message)
            self.stop_plugin()

    @Slot(int)
    def handle_process_finished(self, exit_code):
        """处理插件子进程结束事件"""
        print(f"插件 '{self.plugin_name}' 进程已结束，退出码: {exit_code}")
        if self.toggle_button.isChecked():
            self.toggle_button.setChecked(False)
            if exit_code != 0:
                QMessageBox.critical(self, "插件崩溃", f"插件 '{self.plugin_name}' 进程意外终止！\n\n退出码: {exit_code}")
        self.process = None
        self.output_reader_thread = None

    def stop_plugin(self):
        """停止插件子进程。"""
        self.toggle_button.blockSignals(True)
        self.toggle_button.setChecked(False)
        self.toggle_button.blockSignals(False)
        self.update_button_text()

        if self.process and self.process.poll() is None:
            try:
                self.process.terminate()
                self.process.wait(timeout=1)
            except subprocess.TimeoutExpired:
                self.process.kill()
        if self.output_reader_thread and self.output_reader_thread.isRunning():
            self.output_reader_thread.quit()
        self.process = None

    def set_name(self, new_name):
        """重命名插件"""
        old_name = self.plugin_name
        api_manager = self.plugin_dialog.pet_window.api_manager
        plugin_api = api_manager.get_plugin_api(old_name)
        if plugin_api:
            api_manager.set_plugin_api(new_name, plugin_api.get("api_key"), plugin_api.get("base_url"), plugin_api.get("model"))
            api_manager.remove_plugin_api(old_name)
        self.plugin_name = new_name
        self.name_label.setText(self.plugin_name)

    def __del__(self):
        if QApplication.instance():
            self.stop_plugin()


class DraggablePluginListWidget(QListWidget):
    """拖放功能"""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setDragDropMode(QListWidget.DragDropMode.InternalMove)
        self.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
        self.setAcceptDrops(True)
        self._drag_start_position = QPoint()

    def mousePressEvent(self, event):
        """记录鼠标按下的位置，用于判断是否开始拖动"""
        if event.button() == Qt.MouseButton.LeftButton:
            self._drag_start_position = event.position().toPoint()
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        """当鼠标移动时，如果满足拖动条件，启动拖动操作"""
        if not (event.buttons() & Qt.MouseButton.LeftButton):
            super().mouseMoveEvent(event)
            return
        if (
                event.position().toPoint() - self._drag_start_position).manhattanLength() < QApplication.startDragDistance():
            super().mouseMoveEvent(event)
            return

        item = self.itemAt(self._drag_start_position)
        if not item:
            super().mouseMoveEvent(event)
            return

        widget = self.itemWidget(item)

        pixmap = QPixmap(widget.size())
        pixmap.fill(Qt.GlobalColor.transparent)
        painter = QPainter(pixmap)
        painter.setOpacity(0.75)
        widget.render(painter, QPoint())
        painter.end()

        mime_data = self.model().mimeData(self.selectedIndexes())
        if widget and hasattr(widget, 'plugin_path'):
            urls = [QUrl.fromLocalFile(widget.plugin_path)]
            mime_data.setUrls(urls)

        drag = QDrag(self)
        drag.setMimeData(mime_data)
        drag.setPixmap(pixmap)
        drag.setHotSpot(event.position().toPoint() - self.visualItemRect(item).topLeft())
        drag.exec(Qt.DropAction.CopyAction | Qt.DropAction.MoveAction)


class PluginDialog(QDialog):
    """插件管理框"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("插件库")
        self.setMinimumSize(600, 400)
        self.pet_window = parent
        main_layout = QVBoxLayout(self)
        main_layout.setSpacing(10)
        self.setAcceptDrops(True)
        self.plugin_list_widget = DraggablePluginListWidget(self)
        self.plugin_list_widget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.plugin_list_widget.customContextMenuRequested.connect(self.show_plugin_item_menu)
        self.plugin_list_widget.model().rowsMoved.connect(self.on_plugin_order_changed)
        self.plugin_list_widget.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        main_layout.addWidget(self.plugin_list_widget)

        self.add_plugin_button = QPushButton("导入插件 (.py) 或依赖 (requirements.txt)")
        self.add_plugin_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon))
        self.add_plugin_button.clicked.connect(self.add_files)
        button_layout = QHBoxLayout()
        button_layout.addStretch()
        button_layout.addWidget(self.add_plugin_button)
        button_layout.addStretch()
        main_layout.addLayout(button_layout)
        self.setLayout(main_layout)
        self.load_plugins_config()

    def dragEnterEvent(self, event: QDragEnterEvent):
        """当文件被拖入窗口时，检查文件类型是否支持"""
        mime_data = event.mimeData()
        if mime_data.hasUrls():
            if any(url.toLocalFile().lower().endswith(('.py', '.txt')) for url in mime_data.urls()):
                event.acceptProposedAction()
                return
        event.ignore()

    def dropEvent(self, event: QDropEvent):
        """当文件在窗口中被放下时，处理这些文件。"""
        mime_data = event.mimeData()
        if mime_data.hasUrls():
            urls = [url for url in mime_data.urls() if url.isLocalFile()]
            py_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith('.py')]
            txt_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith('.txt')]

            if py_files:
                self.add_plugins(py_files)

            if txt_files:
                self.install_from_requirements(txt_files[0])

            event.acceptProposedAction()

    def add_files(self):
        """通过文件对话框添加插件或依赖文件"""
        file_paths, _ = QFileDialog.getOpenFileNames(self, "选择插件或依赖文件", "",
                                                     "所有支持的文件 (*.py *.txt);;Python 插件 (*.py);;依赖文件 (*.txt)")
        if not file_paths: return
        py_files = [p for p in file_paths if p.lower().endswith('.py')]
        req_files = [p for p in file_paths if p.lower().endswith('.txt')]
        if req_files: self.install_from_requirements(req_files[0])
        if py_files: self.add_plugins(py_files)

    def install_from_requirements(self, req_path):
        """从requirements.txt文件安装依赖。"""
        if QMessageBox.question(self, "安装依赖",
                                f"你选择了一个依赖文件:\n{os.path.basename(req_path)}\n\n是否要使用当前Python环境安装其中的所有库？",
                                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) == QMessageBox.StandardButton.Yes:
            settings_dialog = self.pet_window.settings_dialog
            settings_dialog.tabs.setCurrentWidget(settings_dialog.environment_tab)
            settings_dialog.install_from_requirements(req_path)
            settings_dialog.show()

    def add_plugins(self, file_paths):
        """处理添加新插件的逻辑"""
        for file_path in file_paths:
            plugin_name_suggestion = os.path.splitext(os.path.basename(file_path))[0]
            try:
                target_file_name = f"{plugin_name_suggestion}.py"
                target_file_path = os.path.join(PLUGIN_STORAGE_DIR, target_file_name)
                base_name, suffix_num = plugin_name_suggestion, 1
                while os.path.exists(target_file_path):
                    target_file_name = f"{base_name}_{suffix_num}.py"
                    target_file_path = os.path.join(PLUGIN_STORAGE_DIR, target_file_name)
                    suffix_num += 1
                import shutil
                shutil.copyfile(file_path, target_file_path)
            except Exception as e:
                QMessageBox.critical(self, "复制失败", f"导入插件时复制文件失败:\n{e}")
                continue
            plugin_name, ok = QInputDialog.getText(self, "插件名称", f"输入插件 '{plugin_name_suggestion}' 的显示名称:",
                                                   QLineEdit.EchoMode.Normal, plugin_name_suggestion)
            if ok and plugin_name:
                if any(os.path.normcase(self.plugin_list_widget.itemWidget(
                        self.plugin_list_widget.item(i)).plugin_path) == os.path.normcase(target_file_path) for i in
                       range(self.plugin_list_widget.count())):
                    QMessageBox.information(self, "插件已存在", f"该插件已在列表中。")
                    continue
                plugin_item_widget = PluginItemWidget(plugin_name, target_file_path, self)
                list_item = QListWidgetItem(self.plugin_list_widget)
                list_item.setSizeHint(plugin_item_widget.sizeHint())
                self.plugin_list_widget.addItem(list_item)
                self.plugin_list_widget.setItemWidget(list_item, plugin_item_widget)
            else:
                try:
                    os.path.exists(target_file_path) and os.remove(target_file_path)
                except Exception as e:
                    print(f"删除未命名插件文件失败: {e}")
        self.save_plugins_config()

    def on_plugin_order_changed(self):
        """当插件顺序改变时，保存配置"""
        self.save_plugins_config()

    def show_plugin_item_menu(self, position):
        """显示插件列表项的右键菜单。"""
        item = self.plugin_list_widget.itemAt(position)
        if not item: return
        plugin_widget = self.plugin_list_widget.itemWidget(item)
        if not isinstance(plugin_widget, PluginItemWidget):
            return
        menu = QMenu(self)
        rename_action = menu.addAction("重命名插件")
        remove_action = menu.addAction("移除插件")
        action = menu.exec(self.plugin_list_widget.mapToGlobal(position))
        if action == rename_action:
            self.rename_plugin(item, plugin_widget)
        elif action == remove_action:
            self.remove_plugin(item, plugin_widget)

    def rename_plugin(self, item, plugin_widget):
        """重命名插件。"""
        old_name = plugin_widget.plugin_name
        new_name, ok = QInputDialog.getText(self, "重命名插件", "输入新的插件名称:", QLineEdit.EchoMode.Normal,
                                            old_name)
        if ok and new_name and new_name != old_name:
            plugin_widget.set_name(new_name)
            self.save_plugins_config()

    def remove_plugin(self, item, plugin_widget):
        """移除插件"""
        if QMessageBox.question(self, "移除插件",
                                f"确定要移除插件 '{plugin_widget.plugin_name}' 吗?\n此操作也会删除其专属API配置。",
                                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) == QMessageBox.StandardButton.Yes:
            self.pet_window.api_manager.remove_plugin_api(plugin_widget.plugin_name)
            plugin_widget.stop_plugin()
            try:
                if os.path.exists(plugin_widget.plugin_path):
                    os.remove(plugin_widget.plugin_path)
            except Exception as e:
                print(f"删除插件文件出错: {e}")
            self.plugin_list_widget.takeItem(self.plugin_list_widget.row(item))
            self.save_plugins_config()

    def save_plugins_config(self):
        """将所有插件的状态保存到JSON配置文件"""
        config = {"plugins": [self.plugin_list_widget.itemWidget(self.plugin_list_widget.item(i)).get_state() for i in
                              range(self.plugin_list_widget.count())]}
        try:
            with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
                json.dump(config, f, indent=4, ensure_ascii=False)
        except IOError as e:
            QMessageBox.warning(self, "保存失败", f"无法保存插件配置: {e}")

    def load_plugins_config(self):
        """从JSON配置文件加载所有插件。"""
        try:
            if os.path.exists(CONFIG_FILE):
                with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                    config_data = json.load(f)
                for p_info in config_data.get("plugins", []):
                    if not os.path.exists(p_info.get("path", "")): continue
                    plugin_item_widget = PluginItemWidget(p_info["name"], p_info["path"], self,
                                                          p_info.get("active", False), p_info.get("in_menu", False),
                                                          p_info.get("use_kb", False))
                    list_item = QListWidgetItem(self.plugin_list_widget)
                    list_item.setSizeHint(plugin_item_widget.sizeHint())
                    self.plugin_list_widget.addItem(list_item)
                    self.plugin_list_widget.setItemWidget(list_item, plugin_item_widget)
        except (IOError, json.JSONDecodeError) as e:
            print(f"加载插件配置时出错: {e}")

    def stop_all_plugins(self):
        """停止所有正在运行的插件"""
        [widget.stop_plugin() for i in range(self.plugin_list_widget.count()) if
         isinstance(widget := self.plugin_list_widget.itemWidget(self.plugin_list_widget.item(i)), PluginItemWidget)]

    def get_plugins_for_menu(self):
        """获取所有标记为“加入菜单”的插件，用于构建右键菜单。"""
        return [widget for i in range(self.plugin_list_widget.count()) if
                isinstance(widget := self.plugin_list_widget.itemWidget(self.plugin_list_widget.item(i)),
                           PluginItemWidget) and widget.menu_checkbox.isChecked()]


class SettingsDialog(QDialog):
    """主设置对话框，多个标签页。"""
    def __init__(self, parent=None, setup_mode=False):
        super().__init__(parent)
        self.setWindowTitle("设置")
        self.setMinimumSize(700, 600)
        self.pet_window = parent
        self.env_manager = self.pet_window.env_manager
        self.api_manager = self.pet_window.api_manager
        self.kb_manager = self.pet_window.kb_manager
        self.pip_thread = None
        self.setup_mode = setup_mode

        # --- 【修改】启用整个对话框的拖放功能 ---
        self.setAcceptDrops(True)

        main_layout = QVBoxLayout(self)
        self.tabs = QTabWidget()
        main_layout.addWidget(self.tabs)
        self.api_tab = QWidget()
        self.kb_tab = QWidget()
        self.appearance_tab = QWidget()
        self.environment_tab = QWidget()
        self.tabs.addTab(self.api_tab, "全局AI设置")
        self.tabs.addTab(self.kb_tab, "知识库管理")
        self.tabs.addTab(self.appearance_tab, "外观设置")
        self.tabs.addTab(self.environment_tab, "Python 环境")
        self.setup_api_ui()
        self.setup_kb_ui()
        self.setup_appearance_ui()
        self.setup_environment_ui()
        self.setLayout(main_layout)
        if self.setup_mode:
            self.tabs.widget(0).setEnabled(False)
            self.tabs.widget(1).setEnabled(False)
            self.tabs.widget(
                2).setEnabled(False)
            self.tabs.setCurrentIndex(3)
        else:
            self.load_settings()
        self.tabs.currentChanged.connect(self.on_tab_changed)

    def dragEnterEvent(self, event: QDragEnterEvent):
        """当文件被拖入窗口时，检查文件类型是否支持。"""
        mime_data = event.mimeData()
        if mime_data.hasUrls():
            # 仅当拖入.txt文件时才接受操作
            if any(url.toLocalFile().lower().endswith('.txt') for url in mime_data.urls()):
                event.acceptProposedAction()
                return
        event.ignore()

    def dropEvent(self, event: QDropEvent):
        """当文件在窗口中被放下时，处理这些文件。"""
        mime_data = event.mimeData()
        if mime_data.hasUrls():
            urls = [url for url in mime_data.urls() if url.isLocalFile()]
            txt_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith('.txt')]

            if txt_files:
                self.tabs.setCurrentWidget(self.environment_tab)
                self.install_from_requirements(txt_files[0])
                event.acceptProposedAction()

    def setup_api_ui(self):
        layout = QVBoxLayout(self.api_tab)
        layout.setSpacing(15)
        layout.addWidget(QLabel("此处为默认的AI API配置，所有插件在未独立设置时都会使用此配置。"))
        api = self.api_manager.get_global_api()
        key_layout = QHBoxLayout()
        key_layout.addWidget(QLabel("全局 API Key:"))
        self.global_api_key_edit = QLineEdit(api.get("api_key", ""))
        key_layout.addWidget(self.global_api_key_edit, 1)
        layout.addLayout(key_layout)
        url_layout = QHBoxLayout()
        url_layout.addWidget(QLabel("全局 Base URL:"))
        self.global_base_url_edit = QLineEdit(api.get("base_url", ""))
        url_layout.addWidget(self.global_base_url_edit, 1)
        layout.addLayout(url_layout)
        model_layout = QHBoxLayout()
        model_layout.addWidget(QLabel("全局默认模型:"))
        self.global_model_edit = QLineEdit(api.get("model", ""))
        model_layout.addWidget(self.global_model_edit, 1)
        layout.addLayout(model_layout)
        save_button = QPushButton("保存全局设置")
        save_button.clicked.connect(self.save_api_settings)
        button_layout = QHBoxLayout()
        button_layout.addStretch()
        button_layout.addWidget(save_button)
        layout.addLayout(button_layout)
        layout.addStretch()

    def save_api_settings(self):
        """保存全局API设置"""
        api_key = self.global_api_key_edit.text().strip();
        base_url = self.global_base_url_edit.text().strip();
        model = self.global_model_edit.text().strip()
        self.api_manager.set_global_api(api_key, base_url, model);
        QMessageBox.information(self, "成功", "全局API设置已保存！")

    def setup_kb_ui(self):
        """构建“知识库管理”标签页的UI。"""
        main_layout = QHBoxLayout(self.kb_tab)
        left_layout = QVBoxLayout()
        right_layout = QVBoxLayout()
        main_layout.addLayout(left_layout, 1)
        separator = QFrame()
        separator.setFrameShape(QFrame.Shape.VLine)
        separator.setFrameShadow(QFrame.Shadow.Sunken)
        main_layout.addWidget(separator)
        main_layout.addLayout(right_layout, 2)
        left_layout.addWidget(QLabel("知识库列表:"))
        self.kb_list_widget = QListWidget()
        self.kb_list_widget.itemDoubleClicked.connect(self.edit_kb_from_list)
        left_layout.addWidget(self.kb_list_widget)
        kb_btn_layout = QHBoxLayout()
        new_btn = QPushButton("新建")
        new_btn.clicked.connect(self.new_kb)
        kb_btn_layout.addWidget(new_btn)
        rename_btn = QPushButton("重命名")
        rename_btn.clicked.connect(self.rename_kb)
        kb_btn_layout.addWidget(rename_btn)
        delete_btn = QPushButton("删除")
        delete_btn.clicked.connect(self.delete_kb)
        kb_btn_layout.addWidget(delete_btn)
        left_layout.addLayout(kb_btn_layout)
        right_layout.addWidget(QLabel("知识库检索辅助AI设置:"))
        helper_api_key_layout = QHBoxLayout()
        helper_api_key_layout.addWidget(QLabel("辅助AI API Key:"))
        self.helper_api_key_edit = QLineEdit()
        self.helper_api_key_edit.setPlaceholderText("留空则使用全局API Key")
        helper_api_key_layout.addWidget(self.helper_api_key_edit)
        right_layout.addLayout(helper_api_key_layout)
        helper_base_url_layout = QHBoxLayout()
        helper_base_url_layout.addWidget(QLabel("辅助AI Base URL:"))
        self.helper_base_url_edit = QLineEdit()
        self.helper_base_url_edit.setPlaceholderText("留空则使用全局Base URL")
        helper_base_url_layout.addWidget(self.helper_base_url_edit)
        right_layout.addLayout(helper_base_url_layout)
        model_layout = QHBoxLayout()
        model_layout.addWidget(QLabel("辅助AI模型:"))
        self.helper_model_edit = QLineEdit()
        self.helper_model_edit.setPlaceholderText("留空则使用全局模型")
        model_layout.addWidget(self.helper_model_edit)
        right_layout.addLayout(model_layout)
        right_layout.addWidget(QLabel("辅助AI指令 (System Prompt):"))
        self.helper_prompt_edit = QTextEdit()
        self.helper_prompt_edit.setAcceptRichText(False)
        self.helper_prompt_edit.setMinimumHeight(200)
        right_layout.addWidget(self.helper_prompt_edit, 1)
        save_btn = QPushButton("保存辅助AI设置")
        save_btn.clicked.connect(self.save_kb_helper_settings)
        btn_layout = QHBoxLayout()
        btn_layout.addStretch()
        btn_layout.addWidget(save_btn)
        right_layout.addLayout(btn_layout)

    def refresh_kb_list(self):
        """刷新知识库列表。"""
        self.kb_list_widget.clear()
        self.kb_list_widget.addItems(self.kb_manager.get_all_bases())

    def new_kb(self):
        """新建知识库"""
        name, ok = QInputDialog.getText(self, "新建知识库", "知识库名称:")
        if ok and name:
            success, msg = self.kb_manager.create_base(name)
            if success:
                self.refresh_kb_list()
            else:
                QMessageBox.warning(self, "创建失败", msg)

    def rename_kb(self):
        """重命名知识库"""
        item = self.kb_list_widget.currentItem()
        if not item:
            QMessageBox.warning(self, "提示", "请先选择一个要重命名的知识库。")
            return
        old_name = item.text()
        new_name, ok = QInputDialog.getText(self, "重命名", "新的知识库名称:", text=old_name)
        if ok and new_name and new_name != old_name:
            success, msg = self.kb_manager.rename_base(old_name, new_name)
            if success:
                if self.kb_manager.get_active_base_name() == old_name: self.kb_manager.set_active_base(new_name)
                self.refresh_kb_list()
            else:
                QMessageBox.warning(self, "重命名失败", msg)

    def delete_kb(self):
        """删除知识库"""
        item = self.kb_list_widget.currentItem()
        if not item: QMessageBox.warning(self, "提示", "请先选择一个要删除的知识库。"); return
        if QMessageBox.question(self, "确认删除", f"确定要永久删除知识库 '{item.text()}' 吗？",
                                QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
            if self.kb_manager.get_active_base_name() == item.text(): self.kb_manager.set_active_base(None)
            success, msg = self.kb_manager.delete_base(item.text())
            if success:
                self.refresh_kb_list()
            else:
                QMessageBox.warning(self, "删除失败", msg)

    def edit_kb_from_list(self, item):
        """双击编辑知识库"""
        self.pet_window.edit_knowledge_base(item.text())

    def load_kb_helper_settings(self):
        """加载辅助AI的设置到UI"""
        config = self.api_manager.get_kb_helper_config()
        self.helper_api_key_edit.setText(config.get("api_key", ""))
        self.helper_base_url_edit.setText(config.get("base_url", ""))
        self.helper_model_edit.setText(config.get("model", ""))
        self.helper_prompt_edit.setPlainText(config.get("prompt", DEFAULT_HELPER_AI_PROMPT))

    def save_kb_helper_settings(self):
        """保存辅助AI的设置"""
        api_key = self.helper_api_key_edit.text().strip()
        base_url = self.helper_base_url_edit.text().strip()
        model = self.helper_model_edit.text().strip()
        prompt = self.helper_prompt_edit.toPlainText().strip()
        self.api_manager.set_kb_helper_config(api_key, base_url, model, prompt)
        QMessageBox.information(self, "成功", "辅助AI设置已保存！")

    def setup_appearance_ui(self):
        """构建“外观设置”标签页的UI"""
        layout = QVBoxLayout(self.appearance_tab)
        layout.setSpacing(15)
        self.change_pet_button = QPushButton("更换宠物外观 (图片/GIF)")
        self.change_pet_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ComputerIcon))
        self.change_pet_button.clicked.connect(self.change_pet_appearance)
        layout.addWidget(self.change_pet_button)
        size_layout = QHBoxLayout()
        size_layout.addWidget(QLabel("宠物显示大小:"))
        self.size_slider = QSlider(Qt.Orientation.Horizontal)
        self.size_slider.setMinimum(50)
        self.size_slider.setMaximum(200)
        if not self.setup_mode: self.size_slider.setValue(int(self.pet_window.current_pet_scale * 100))
        self.size_slider.valueChanged.connect(self.on_size_slider_changed)
        size_layout.addWidget(self.size_slider, 1)
        self.size_value_label = QLabel(f"{self.size_slider.value()}%")
        self.size_value_label.setMinimumWidth(40)
        size_layout.addWidget(self.size_value_label)
        layout.addLayout(size_layout)
        layout.addStretch()

    def setup_environment_ui(self):
        """构建“Python环境”标签页的UI"""
        layout = QVBoxLayout(self.environment_tab)
        layout.setSpacing(10)
        env_path_layout = QHBoxLayout()
        env_path_layout.addWidget(QLabel("当前Python解释器:"))
        self.env_path_line_edit = QLineEdit()
        self.env_path_line_edit.setReadOnly(True)
        env_path_layout.addWidget(self.env_path_line_edit, 1)
        change_btn = QPushButton("更改...")
        change_btn.clicked.connect(self.change_python_env)
        env_path_layout.addWidget(change_btn)
        create_default_btn = QPushButton("创建默认环境")
        create_default_btn.clicked.connect(self.create_default_env)
        env_path_layout.addWidget(create_default_btn)
        layout.addLayout(env_path_layout)
        pip_mirror_layout = QHBoxLayout()
        pip_mirror_layout.addWidget(QLabel("Pip 镜像源:"))
        self.pip_mirror_line_edit = QLineEdit()
        self.pip_mirror_line_edit.setPlaceholderText("例如: https://pypi.tuna.tsinghua.edu.cn/simple")
        self.pip_mirror_line_edit.textChanged.connect(self.save_pip_mirror)
        pip_mirror_layout.addWidget(self.pip_mirror_line_edit, 1)
        layout.addLayout(pip_mirror_layout)
        layout.addWidget(QLabel("已安装的库:"))
        self.package_list_widget = QListWidget()
        layout.addWidget(self.package_list_widget, 1)
        package_btn_layout = QHBoxLayout()
        export_btn = QPushButton("导出环境到")
        export_btn.clicked.connect(self.export_environment)
        package_btn_layout.addWidget(export_btn)
        package_btn_layout.addStretch()
        install_req_btn = QPushButton("导入依赖")
        install_req_btn.clicked.connect(self.install_from_requirements_dialog)
        package_btn_layout.addWidget(install_req_btn)
        install_pkg_btn = QPushButton("安装新库")
        install_pkg_btn.clicked.connect(self.install_package)
        package_btn_layout.addWidget(install_pkg_btn)
        uninstall_pkg_btn = QPushButton("卸载选中库")
        uninstall_pkg_btn.clicked.connect(self.uninstall_package)
        package_btn_layout.addWidget(uninstall_pkg_btn)
        refresh_btn = QPushButton("刷新列表")
        refresh_btn.clicked.connect(self.refresh_package_list)
        package_btn_layout.addWidget(refresh_btn)
        layout.addLayout(package_btn_layout)
        layout.addWidget(QLabel("Pip 日志:"))
        self.log_output = QTextEdit()
        self.log_output.setReadOnly(True)
        layout.addWidget(self.log_output)

    def load_settings(self):
        """加载所有设置到UI"""
        self.refresh_environment_ui()
        self.pip_mirror_line_edit.setText(
            self.env_manager.settings.value("pipIndexUrl", ""))
        self.refresh_kb_list()
        self.load_kb_helper_settings()

    def save_pip_mirror(self):
        """实时保存pip镜像源地址"""
        self.env_manager.settings.setValue("pipIndexUrl", self.pip_mirror_line_edit.text())

    def on_tab_changed(self, index):
        """当标签页切换时，刷新对应页面的数据"""
        if self.tabs.tabText(index) == "Python 环境":
            self.refresh_package_list()
        elif self.tabs.tabText(index) == "知识库管理":
            self.refresh_kb_list()

    def refresh_environment_ui(self):
        """刷新环境路径显示"""
        self.env_path_line_edit.setText(self.env_manager.python_executable or "未找到或未配置")

    def change_python_env(self):
        """通过文件对话框更改Python解释器"""
        path, _ = QFileDialog.getOpenFileName(self, "选择Python解释器", "", "Python Executable (python.exe *.pyw)")
        if path and self.env_manager.set_python_executable(path):
            QMessageBox.information(self, "成功",
                                    "Python环境已更新！")
            self.refresh_environment_ui()
            self.refresh_package_list()
        elif path:
            QMessageBox.warning(self, "失败", "选择的不是一个有效的Python解释器文件。")

    def create_default_env(self):
        """创建默认的.venv虚拟环境"""
        base_python_path, _ = QFileDialog.getOpenFileName(self, "请选择用于创建虚拟环境的Python版本", "",
                                                          "Python Executable (python.exe)")
        if not base_python_path: return
        if QMessageBox.question(self, "确认",
                                f"将使用:\n{base_python_path}\n在应用目录下创建新的虚拟环境(.venv)，并覆盖旧环境。\n确定吗？",
                                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) == QMessageBox.StandardButton.Yes:
            progress = QProgressDialog("正在创建环境...", "取消", 0, 0, self)
            progress.setWindowModality(Qt.WindowModality.WindowModal)
            progress.show()
            QApplication.processEvents()
            success, msg = self.env_manager.create_default_venv(progress.setLabelText, base_python_path)
            progress.close()
            if success:
                QMessageBox.information(self, "成功", msg)
                self.refresh_environment_ui()
                self.refresh_package_list()
            else:
                QMessageBox.critical(self, "失败", msg)

    def refresh_package_list(self):
        """刷新已安装库的列表"""
        self.package_list_widget.clear()
        self.package_list_widget.addItem("正在刷新...")
        pip_path = self.env_manager.get_pip_executable_path()
        if not pip_path:
            self.package_list_widget.clear()
            self.package_list_widget.addItem("无法找到pip，请检查环境配置。")
            return
        self.run_pip_command([pip_path, 'list'], on_finish=self._populate_package_list)

    def _populate_package_list(self, result_text):
        """用 `pip list` 的结果填充列表。"""
        self.package_list_widget.clear()
        lines = result_text.splitlines()
        start_index = 0
        for i, line in enumerate(lines):
            if "---" in line:
                start_index = i + 1
                break
        package_lines = lines[start_index:]
        if not package_lines or all(not line.strip() for line in package_lines):
            self.package_list_widget.addItem("未安装任何库。")
        else:
            [self.package_list_widget.addItem(line.strip()) for line in package_lines if line.strip()]

    def install_package(self):
        """安装新的Python库"""
        package_name, ok = QInputDialog.getText(self, "安装新库", "输入要安装的库名:")
        if ok and package_name:
            pip_path = self.env_manager.get_pip_executable_path()
            if pip_path:
                self.run_pip_command([pip_path, 'install', package_name])

    def install_from_requirements_dialog(self):
        """通过文件对话框选择并安装依赖"""
        path, _ = QFileDialog.getOpenFileName(self, "选择依赖文件", "", "Requirements File (requirements.txt)")
        if path: self.install_from_requirements(path)

    def install_from_requirements(self, req_path):
        """从指定的requirements.txt安装依赖。"""
        pip_path = self.env_manager.get_pip_executable_path()
        pip_path and self.run_pip_command([pip_path, 'install', '-r', req_path])

    def uninstall_package(self):
        """卸载选中的库。"""
        selected_items = self.package_list_widget.selectedItems()
        if not selected_items:
            QMessageBox.warning(self, "提示", "请先在列表中选择一个要卸载的库。")
            return
        package_name = selected_items[0].text().split()[0]
        if QMessageBox.question(self, "确认卸载", f"确定要卸载库 '{package_name}' 吗？",
                                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) == QMessageBox.StandardButton.Yes:
            pip_path = self.env_manager.get_pip_executable_path()
            pip_path and self.run_pip_command([pip_path, 'uninstall', '-y', package_name])

    def export_environment(self):
        """将当前环境的库导出到 requirements.txt。"""
        save_path, _ = QFileDialog.getSaveFileName(self, "导出环境到 requirements.txt", "requirements.txt", "Text File (*.txt)")
        if not save_path:
            return
        pip_path = self.env_manager.get_pip_executable_path()
        if not pip_path:
            QMessageBox.critical(self, "错误", "找不到pip.exe，无法导出。")
            return

        def write_to_file(result_text):
            try:
                with open(save_path, 'w', encoding='utf-8') as f:
                    f.write(result_text)
                QMessageBox.information(self, "成功", f"环境已成功导出到:\n{save_path}")
            except Exception as e:
                QMessageBox.critical(self, "写入文件失败", f"无法将环境写入文件:\n{e}")

        self.run_pip_command([pip_path, 'freeze'], on_finish=write_to_file)

    def run_pip_command(self, command, on_finish=None):
        """执行一个pip命令，并处理UI状态（禁用/启用按钮、显示日志）"""
        if self.pip_thread and self.pip_thread.isRunning():
            QMessageBox.information(self, "请稍候", "另一个pip任务正在运行。")
            return
        # 如果是安装命令，并且设置了镜像源，则添加镜像源参数
        mirror_url = self.pip_mirror_line_edit.text().strip()
        if mirror_url and command[1] == 'install':
            command.extend(['--index-url', mirror_url])
            parsed_url = urlparse(mirror_url)
            if parsed_url.hostname:
                command.extend(['--trusted-host', parsed_url.hostname])  # 信任非https的镜像源主机
        self.log_output.clear()
        self.pip_thread = PipTaskThread(command, self)
        # 连接信号到槽函数
        self.pip_thread.task_started.connect(self.log_output.append)
        self.pip_thread.task_output.connect(self.log_output.append)
        temp_log = []  # 临时存储日志，用于传递给on_finish回调
        self.pip_thread.task_output.connect(lambda line: temp_log.append(line))

        def final_handler(finish_message):
            self.log_output.append(finish_message)
            self.set_buttons_enabled(True)
            if on_finish:
                on_finish('\n'.join(temp_log))
            else:
                QTimer.singleShot(500, self.refresh_package_list)

        self.pip_thread.task_finished.connect(final_handler)
        self.pip_thread.start()
        self.set_buttons_enabled(False)

    def set_buttons_enabled(self, enabled):
        """启用或禁用环境标签页中的所有按钮"""
        [btn.setEnabled(enabled) for btn in self.environment_tab.findChildren(QPushButton)]

    def change_pet_appearance(self):
        """更改助手外观"""
        if self.setup_mode: return
        file_path, _ = QFileDialog.getOpenFileName(self, "选择助手图片或GIF", "", "图像文件 (*.png *.jpg *.jpeg *.bmp *.gif);;所有文件 (*)")
        if file_path:
            self.pet_window.load_pet_appearance(file_path)
            self.pet_window.save_app_settings()

    @Slot(int)
    def on_size_slider_changed(self, value):
        """处理尺寸滑块的值变化。"""
        if self.setup_mode: return
        self.size_value_label.setText(f"{value}%")
        self.pet_window.set_pet_scale(value / 100.0)


class DesktopPet(QWidget):
    """应用程序的主窗口类，助手本体"""
    def __init__(self):
        super().__init__()
        self.setObjectName("DesktopPet")
        self.app_settings = QSettings("MyCompany", "DesktopPetAppPySide6Enhanced")
        self.env_manager = EnvironmentManager(self.app_settings)
        self.api_manager = ApiManager(API_CONFIG_FILE)
        self.kb_manager = KnowledgeBaseManager(KNOWLEDGE_BASE_DIR, self.app_settings)
        self.pet_label = QLabel(self)
        self.pet_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.original_pixmap, self.movie, self.current_pet_path, self.current_pet_scale = None, None, "", 1.0
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool)
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        palette = self.palette()
        palette.setColor(QPalette.ColorRole.Window, Qt.GlobalColor.transparent)
        self.setPalette(palette)
        self.setAutoFillBackground(True)
        main_layout = QVBoxLayout(self)
        main_layout.addWidget(self.pet_label)
        main_layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(main_layout)
        self._drag_start_position = None
        # 消息气泡
        self.bubble = BubbleLabel(self)
        self.load_app_settings()
        self.plugin_dialog = PluginDialog(self)
        self.settings_dialog = SettingsDialog(self)
        # 拖放功能
        self.setAcceptDrops(True)

        self.kb_api_service = KnowledgeApiService(self.api_manager, self.kb_manager)
        # 将API服务线程的信号连接到主窗口的槽函数
        self.kb_api_service.message_from_plugin_received.connect(self.show_bubble_message)
        self.kb_api_service.start()

    @Slot(str)
    def show_bubble_message(self, text: str):
        """在助手头顶显示消息"""
        self.bubble.show_message(text)

    def dragEnterEvent(self, event: QDragEnterEvent):
        """当有东西被拖入宠物控件范围时，决定是否接受"""
        # 如果拖入的是文件(URLs)或文本，就显示一个可接受的图标
        if event.mimeData().hasUrls() or event.mimeData().hasText():
            event.acceptProposedAction()
        else:
            event.ignore()

    def dragMoveEvent(self, event: QDragEnterEvent):
        """当拖动在控件内移动时"""
        event.acceptProposedAction()

    def _handle_knowledge_base_drop(self, mime_data: QMimeData):
        """处理将文本或.txt文件添加到知识库的逻辑"""
        active_base = self.kb_manager.get_active_base_name()
        if not active_base:
            QMessageBox.warning(self, "操作失败", "请先在右键菜单中选择一个要操作的知识库！")
            return
        text_to_add = None
        if mime_data.hasUrls():
            url = mime_data.urls()[0]
            if url.isLocalFile():
                file_path = url.toLocalFile()
                if file_path.lower().endswith('.txt'):
                    try:
                        with open(file_path, 'r', encoding='utf-8') as f:
                            text_to_add = f.read()
                    except Exception as e:
                        QMessageBox.critical(self, "读取失败", f"无法读取文件内容:\n{e}")
                else:
                    # 如果不是.txt文件，对于知识库操作则忽略
                    QMessageBox.warning(self, "格式不支持", "只能拖入 .txt 文件来添加知识。")
        elif mime_data.hasText():
            text_to_add = mime_data.text()
        if text_to_add:
            success, msg = self.kb_manager.append_to_base(active_base, text_to_add)
            if success:
                self.show_bubble_message(f"信息已添加至: {active_base}")
            else:
                QMessageBox.warning(self, "添加失败", msg)

    def dropEvent(self, event: QDropEvent):
        """当文件或文本被释放在助手身上时，根据类型执行不同操作。"""
        mime_data = event.mimeData()

        if mime_data.hasUrls():
            urls = [url for url in mime_data.urls() if url.isLocalFile()]
            py_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith('.py')]
            txt_files = [url.toLocalFile() for url in urls if url.toLocalFile().lower().endswith('.txt')]

            if py_files:
                self.plugin_dialog.add_plugins(py_files)
                if not self.plugin_dialog.isVisible():
                    self.plugin_dialog.show()
                event.acceptProposedAction()

            if txt_files:
                for txt_file in txt_files:
                    if os.path.basename(txt_file).lower() == 'requirements.txt':
                        msg_box = QMessageBox(self)
                        msg_box.setWindowTitle("文件操作选择")
                        msg_box.setText(f"如何处理文件 '{os.path.basename(txt_file)}'?")
                        install_btn = msg_box.addButton("导入库", QMessageBox.ButtonRole.ActionRole)
                        kb_btn = msg_box.addButton("导入知识库", QMessageBox.ButtonRole.ActionRole)
                        cancel_btn = msg_box.addButton("取消", QMessageBox.ButtonRole.RejectRole)
                        msg_box.exec()
                        clicked_button = msg_box.clickedButton()
                        if clicked_button == install_btn:
                            self.settings_dialog.tabs.setCurrentWidget(self.settings_dialog.environment_tab)
                            self.settings_dialog.install_from_requirements(txt_file)
                            if not self.settings_dialog.isVisible():
                                self.settings_dialog.show()
                        elif clicked_button == kb_btn:
                            temp_mime = QMimeData()
                            temp_mime.setUrls([QUrl.fromLocalFile(txt_file)])
                            self._handle_knowledge_base_drop(temp_mime)
                    else:
                        temp_mime = QMimeData()
                        temp_mime.setUrls([QUrl.fromLocalFile(txt_file)])
                        self._handle_knowledge_base_drop(temp_mime)
                event.acceptProposedAction()

            if py_files or txt_files:
                return

        if mime_data.hasText() and not mime_data.hasUrls():
            self._handle_knowledge_base_drop(mime_data)
            event.acceptProposedAction()

    def create_red_exit_icon(self):
        """动态绘制一个红色的叉，用于退出按钮"""
        pixmap = QPixmap(16, 16)
        pixmap.fill(Qt.GlobalColor.transparent)
        painter = QPainter(pixmap)
        pen = QPen(QColor("#F44336"))  # 使用主题中的红色
        pen.setWidth(2)
        pen.setCapStyle(Qt.PenCapStyle.RoundCap)
        painter.setPen(pen)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        painter.drawLine(4, 4, 12, 12)
        painter.drawLine(12, 4, 4, 12)
        painter.end()
        return QIcon(pixmap)

    def contextMenuEvent(self, event):
        """处理右键菜单事件"""
        menu = QMenu(self)
        style = self.style()

        if hasattr(self, 'plugin_dialog') and self.plugin_dialog:
            plugins_in_menu = self.plugin_dialog.get_plugins_for_menu()
            if plugins_in_menu:
                for widget in plugins_in_menu:
                    action = QAction(widget.plugin_name, menu)
                    if widget.toggle_button.isChecked():
                        if os.path.exists(CUSTOM_STOP_ICON_PATH):
                            action.setIcon(QIcon(CUSTOM_STOP_ICON_PATH))
                        else:
                            action.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_MediaStop))
                    else:
                        if os.path.exists(CUSTOM_PLAY_ICON_PATH):
                            action.setIcon(QIcon(CUSTOM_PLAY_ICON_PATH))
                        else:
                            action.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_MediaPlay))

                    action.setCheckable(True)
                    action.setChecked(widget.toggle_button.isChecked())
                    action.toggled.connect(widget.toggle_button.toggle)
                    menu.addAction(action)
                menu.addSeparator()

        kb_menu = menu.addMenu("知识库")
        kb_menu.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_FileIcon))
        active_base_name = self.kb_manager.get_active_base_name()
        kb_action_group = QActionGroup(self)
        kb_action_group.setExclusive(True)
        kb_action_group.triggered.connect(self.setActiveKnowledgeBase)
        all_bases = self.kb_manager.get_all_bases()
        if not all_bases:
            kb_menu.addAction("无知识库").setEnabled(False)
        else:
            for base_name in all_bases:
                action = QAction(base_name, kb_menu)
                action.setCheckable(True)
                action.setChecked(base_name == active_base_name)
                kb_action_group.addAction(action)
                kb_menu.addAction(action)
        kb_menu.addSeparator()
        edit_kb_action = kb_menu.addAction("编辑当前库...")
        edit_kb_action.setEnabled(bool(active_base_name))
        edit_kb_action.triggered.connect(lambda: self.edit_knowledge_base(self.kb_manager.get_active_base_name()))
        manage_kb_action = kb_menu.addAction("从设置面板管理...")
        manage_kb_action.triggered.connect(
            lambda: (self.settings_dialog.tabs.setCurrentIndex(1), self.settings_dialog.exec()))
        menu.addSeparator()

        plugin_lib_action = QAction("插件库", self)
        plugin_lib_action.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_FileDialogListView))
        menu.addAction(plugin_lib_action)

        settings_action = QAction("设置", self)
        settings_action.setIcon(style.standardIcon(QStyle.StandardPixmap.SP_FileDialogDetailedView))
        menu.addAction(settings_action)

        menu.addSeparator()

        exit_action = QAction("退出", self)
        exit_action.setIcon(self.create_red_exit_icon())
        menu.addAction(exit_action)

        action = menu.exec(self.mapToGlobal(event.pos()))
        if action == plugin_lib_action:
            self.plugin_dialog.exec()
        elif action == settings_action:
            self.settings_dialog.exec()
        elif action == exit_action:
            self.close()

    @Slot(QAction)
    def setActiveKnowledgeBase(self, action):
        """设置当前激活的知识库"""
        base_name = action.text()
        if action.isChecked():
            self.kb_manager.set_active_base(base_name)
            self.show_bubble_message(f"知识库切换: {base_name}")
            print(f"当前知识库已切换为: {base_name}")
        else:
            self.kb_manager.set_active_base(None)
            self.show_bubble_message("知识库已禁用")
            print("已取消所有知识库选择")

    def edit_knowledge_base(self, base_name):
        """打开知识库编辑器"""
        if not base_name:
            return
        content = self.kb_manager.get_base_content_by_name(base_name) or ""
        editor_dialog = KnowledgeBaseEditorDialog(base_name, content, self)
        if editor_dialog.exec():
            new_content = editor_dialog.get_content()
            success, msg = self.kb_manager.update_base_content(base_name, new_content)
            if not success: QMessageBox.warning(self, "错误", f"保存知识库失败: {msg}")

    def _create_fallback_pixmap(self):
        """创建一个笑脸，作为默认的宠物外观图像"""
        pixmap = QPixmap(DEFAULT_PET_BASE_WIDTH, DEFAULT_PET_BASE_HEIGHT)
        pixmap.fill(Qt.GlobalColor.transparent)
        painter = QPainter(pixmap)
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        painter.setBrush(QBrush(QColor(255, 223, 186)))
        painter.drawEllipse(QRect(10, 10, 80, 80))
        painter.setBrush(Qt.GlobalColor.black)
        eye_y, eye_size = 33, 10
        painter.drawEllipse(QRect(30 - 5, eye_y - 5, eye_size, eye_size))
        painter.drawEllipse(QRect(70 - 5, eye_y - 5, eye_size, eye_size))
        painter.drawArc(QRect(25, 50, 50, 30), 0, -180 * 16)
        painter.end()
        return pixmap

    def load_pet_appearance(self, file_path):
        """从文件加载助手外观"""
        if self.movie:
            self.movie.stop()
            self.movie = None
        self.original_pixmap = None
        self.pet_label.clear()
        if not os.path.exists(file_path):
            self.original_pixmap = self._create_fallback_pixmap()
            self.current_pet_path = ""
        elif file_path.lower().endswith(".gif"):
            self.movie = QMovie(file_path)
            if not self.movie.isValid():
                self.movie = None
                self.original_pixmap = self._create_fallback_pixmap()
                self.current_pet_path = ""
            else:
                self.current_pet_path = file_path
        else:
            loaded_pixmap = QPixmap(file_path)
            if loaded_pixmap.isNull():
                self.original_pixmap = self._create_fallback_pixmap()
                self.current_pet_path = ""
            else:
                self.original_pixmap = loaded_pixmap
                self.current_pet_path = file_path
        self.apply_pet_scale_and_display()

    def apply_pet_scale_and_display(self):
        """根据当前的缩放比例，应用并显示宠物外观"""
        target_width, target_height = int(DEFAULT_PET_BASE_WIDTH * self.current_pet_scale), int(
            DEFAULT_PET_BASE_HEIGHT * self.current_pet_scale)
        if self.movie and self.movie.isValid():
            self.pet_label.setMovie(self.movie)
            self.movie.setCacheMode(QMovie.CacheMode.CacheAll)
            original_frame_size = self.movie.currentPixmap().size()
            if original_frame_size.isEmpty() and self.movie.frameCount() > 0:
                self.movie.jumpToNextFrame()
                original_frame_size = self.movie.currentPixmap().size()
            if original_frame_size.isEmpty():
                original_frame_size = QSize(DEFAULT_PET_BASE_WIDTH, DEFAULT_PET_BASE_HEIGHT)
            scaled_size = original_frame_size.scaled(target_width, target_height, Qt.AspectRatioMode.KeepAspectRatio)
            self.pet_label.setFixedSize(scaled_size)
            self.pet_label.setScaledContents(True)
            self.resize(scaled_size)
            self.movie.start()
        elif self.original_pixmap and not self.original_pixmap.isNull():
            scaled_pixmap = self.original_pixmap.scaled(target_width, target_height, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
            self.pet_label.setPixmap(scaled_pixmap)
            self.resize(scaled_pixmap.size())
        else:
            fallback_pixmap = self._create_fallback_pixmap()
            scaled_fallback = fallback_pixmap.scaled(target_width, target_height, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
            self.pet_label.setPixmap(scaled_fallback)
            self.resize(scaled_fallback.size())

    def set_pet_scale(self, scale_factor):
        """设置助手缩放比例"""
        self.current_pet_scale = max(0.1, scale_factor)
        self.apply_pet_scale_and_display()
        self.save_app_settings()

    # 窗口拖动事件
    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self._drag_start_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
            event.accept()

    def mouseMoveEvent(self, event):
        if event.buttons() == Qt.MouseButton.LeftButton and self._drag_start_position is not None:
            self.move(event.globalPosition().toPoint() - self._drag_start_position)
            event.accept()

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton and self._drag_start_position is not None:
            self._drag_start_position = None
            self.save_app_settings()
            event.accept()

    def moveEvent(self, event):
        """窗口移动时，重新定位消息气泡"""
        super().moveEvent(event)
        if hasattr(self, 'bubble'):
            self.bubble.reposition()

    def resizeEvent(self, event):
        """窗口大小改变时，重新定位消息气泡"""
        super().resizeEvent(event)
        if hasattr(self, 'bubble'):
            QTimer.singleShot(10, self.bubble.reposition)

    def load_app_settings(self):
        """从QSettings加载应用设置"""
        self.current_pet_scale = float(self.app_settings.value("petScale", 1.0))
        default_fallback_path = ""
        if os.path.exists(DEFAULT_PET_IMAGE_FALLBACK):
            default_fallback_path = DEFAULT_PET_IMAGE_FALLBACK
        pet_path_setting = self.app_settings.value("petAppearancePath", default_fallback_path)
        if pet_path_setting and os.path.exists(pet_path_setting):
            self.load_pet_appearance(pet_path_setting)
        elif default_fallback_path:
            self.load_pet_appearance(default_fallback_path)
        else:
            self.original_pixmap = self._create_fallback_pixmap()
            self.current_pet_path = ""
            self.apply_pet_scale_and_display()
        pos_setting = self.app_settings.value("windowPosition", QPoint(100, 100))
        if isinstance(pos_setting, QPoint): self.move(pos_setting)

    def save_app_settings(self):
        """保存应用设置到QSettings"""
        if hasattr(self, 'current_pet_path') and self.current_pet_path:
            self.app_settings.setValue("petAppearancePath", self.current_pet_path)
        self.app_settings.setValue("windowPosition", self.pos())
        self.app_settings.setValue("petScale", self.current_pet_scale)
        self.app_settings.sync()

    def closeEvent(self, event):
        """窗口关闭事件。"""
        if hasattr(self, 'bubble'):
            self.bubble.hide()
        self.kb_api_service.stop()
        self.close_application()
        event.accept()

    def close_application(self):
        """执行程序关闭前的清理工作"""
        print("正在关闭应用程序...")
        if hasattr(self, 'plugin_dialog') and self.plugin_dialog:
            self.plugin_dialog.stop_all_plugins()
        self.save_app_settings()
        if QApplication.instance():
            QApplication.instance().quit()


if __name__ == '__main__':
    if sys.platform == 'win32':
        freeze_support()
    app = QApplication(sys.argv)
    app.setStyleSheet(STYLESHEET)
    temp_settings = QSettings("MyCompany", "DesktopPetAppPySide6Enhanced")
    env_manager = EnvironmentManager(temp_settings)
    if not env_manager.python_executable:
        QMessageBox.information(None, "首次启动向导", "未检测到已配置的Python环境。\n\n请在接下来的窗口中，点击“创建默认环境”或“更改”来指定一个有效的Python环境。")
        temp_main_window = QWidget()
        temp_main_window.app_settings = temp_settings
        temp_main_window.env_manager = env_manager
        temp_main_window.api_manager = ApiManager(API_CONFIG_FILE)
        temp_main_window.kb_manager = KnowledgeBaseManager(KNOWLEDGE_BASE_DIR, temp_settings)
        settings_dialog = SettingsDialog(temp_main_window, setup_mode=True)
        settings_dialog.exec()
        env_manager.python_executable = env_manager._find_python_executable()
        if not env_manager.python_executable:
            QMessageBox.critical(None, "致命错误", "未配置有效的Python环境，程序即将退出。")
            sys.exit(1)

    pet = DesktopPet()
    pet.show()
    sys.exit(app.exec())